[iOS]UITableView,UICollectionViewでドラッグ&ドロップする
配列モデルの順序をドラッグで変更
iOSでは静的な配列モデルの表現機能だけではなく、モデルの順序を動的に実行時に動かすようにするための仕組みが搭載されており、ユーザーの操作に合わせて配列の順序を入れ替えるように制御することができます。
UIKitの標準的なコンポーネントUITableViewではモデルの順序を変更する機能は備わっていますが、順序変更の際の挙動として、ドラッグ領域が限られていたり、アイコンが表示されるなどあまり使い勝手の良い物ではありません。 また、UICollectionViewには編集モードが搭載されておらず、標準的なやり方ではドラッグ&ドロップを実装する為に労力が必要です。
そこで本記事では動的に順序を変更するという操作に絞ってドラッグによる順序変更の補助となるOSSを紹介します。
今回例として取り上げるソースの入ったサンプルプロジェクトはGithubに上がっています。
前提環境
- Mac OS X 10.9.2 Mavericks
- Xcode 5.1.1
- CocoaPods 0.32
BVReorderTableView
導入方法
おなじみのCocoaPodsです
Podfile
pod 'BVReorderTableView'
おおまかな実装方法
UITableViewのサブクラスであるBVReorderTableViewを用います。今回は例としてStoryboardを使って実装します。
まず、StoryBoardに追加したUIViewControllerにUITableViewを追加して、そのClassをBVReorderTableViewに変更します。
UIViewControllerのサブクラスをReorderTableViewDelegateのプロトコルに適合するようにします。このプロトコルはUITableViewDelegateを継承しており、改めてUITableViewDelegateのプロトコル宣言をする必要がありません。
MYTableViewController.m
#import "BVReorderTableView.h" @interface MYTableViewController () <ReorderTableViewDelegate, UITableViewDataSource> /** * 動物モデル管理リポジトリ */ @property (nonatomic) MYAnimalRepository *repository; @end
StoryBoardのBVReorderTableViewからdelegateとdataSourceをMYTableViewControllerのクラスに引っ張ってくるのを忘れないようにしてください。
ReorderTableViewDelegateの各種メソッドを実装します。配列モデルは入れ替え可能になるような口を用意しておくことが前提になっています。
#pragma mark - ReorderTableViewDelegate /** * 順序入れ替えプロセスが始まった時に呼ばれるメソッドです。 * 指定インデックスパスに応じてデータソースモデルにブランクオブジェクトを入れ、 * もともと入っていたオブジェクトを保存するために返り値として返します。 * * @param indexPath 入れ替えプロセスが始まった時にドラッグされたセルのインデックスパス * * @return ドラッグ中のセルに入る一時的な保存対象のモデルオブジェクト */ - (id)saveObjectAndInsertBlankRowAtIndexPath:(NSIndexPath *)indexPath { //セルの入れ替えの際に生じる空のセルを処理するために、空のセル用のオブジェクトを用意して //もともとの配列モデルの要素と入れ替えます。返り値として元のオブジェクトを返し、保存します。 NSUInteger saveAnimalIndex = indexPath.row; MYAnimal *savedAnimal = self.repository.animals[saveAnimalIndex]; MYAnimal *blankAnimal = [MYAnimalFactory new].blankAnimal; [self.repository replaceAnimalAtIndex:saveAnimalIndex withAnimal:blankAnimal]; return savedAnimal; } /** * 順序入れ替えプロセスの中で選択されたセルが新しい位置に収まった時に毎回呼ばれます。 * メソッドのコールに応じてデータソースモデルに順序入れ替えの指示をここで出します。 * * @param fromIndexPath 順序入れ替え元のセルインデックスパス * @param toIndexPath 順序入れ替え先のセルインデックスパス */ - (void)moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath { //モデルに順序入れ替えの指示を出します。 [self.repository moveAnimalAtIndex:fromIndexPath.row toIndex:toIndexPath.row]; } /** * 順序入れ替えプロセスの最後、選択されたセルが最終的な位置に収まった時に呼ばれます。 * saveObjectAndInsertBlankRowAtIndexPathで保存されたオブジェクトが引数として返ってくるので、 * こちらのメソッド内でデータソースに挿入されたブランクオブジェクトと入れ替えてください。 * * @param animal 一時的にセーブされたモデルオブジェクト * @param indexPath ドラッグされていたセルが最終的に収まる先のインデックスパス */ - (void)finishReorderingWithObject:(id)object atIndexPath:(NSIndexPath *)indexPath { //元のオブジェクトを復元します。 [self.repository replaceAnimalAtIndex:indexPath.row withAnimal:object]; }
ドラッグ中のオブジェクトに相当するブランクオブジェクトを元のデータソースモデルに挿入する仕組みをここでは実装しています。
MYAnimalRepositoryの実装についてはこちらでは解説しません。NSMutableArrayのラッバーのようなものと考えてもらえればいいです。 後はUITableViewDataSourceの必要なメソッドを宣言したり、モデルデータの初期化等があります。よくある実装方法ですのでこちらでは解説を割愛します。
あとはモデルの要素を表現するカスタムセルにブランクオブジェクト表示のための仕組みを実装すれば完了です。カスタムセルのコードを一部抜き出します。
MYAnimalTableViewCell.h
/** * 動物に関する情報を表示するためのTableViewセルです。 */ @interface MYAnimalTableViewCell : UITableViewCell /** * 表示対象の動物 */ @property (nonatomic) MYAnimal *animal; @end
MYAnimalTableViewCell.m
@implementation MYAnimalTableViewCell #pragma mark - Accessor methods - (void)setAnimal:(MYAnimal *)animal { _animal = animal; self.nameLabel.text = animal.name; [self.nameLabel sizeToFit]; //ブランクオブジェクト特有の処理です。 if (animal.age == -1) { self.ageLabel.text = @""; } else { self.ageLabel.text = [@(animal.age) stringValue]; } [self.ageLabel sizeToFit]; } @end
モデルデータにブランクオブジェクトが入っていた時の表示方法をこちらで決めています。サンプルではブランクオブジェクトに対して、セル全体が白く見えるようにしています。
上記のような実装を経たサンプルプロジェクトのコードを動かしてみます。
上のようにセルがドラッグ可能になっていることがわかります。
DraggableCollectionView
導入方法
BVReorderTableViewと同様です。
Podfile
pod 'DraggableCollectionView'
大まかな実装方法
BVReorderTableViewとは異なり、UICollectionView自体はサブクラス化することなく実現できます。
まずStoryboardでUIViewControllerのサブクラスにおいたUICollectionViewの中にUICollectionViewFlowLayoutと書かれたオブジェクトがあるかと思います。こちらをDraggableCollectionViewFlowLayoutに変更してください。
UICollectionViewの置かれたサブクラスをUICollectionViewDataSource_Draggable, UICollectionViewDelegateプロトコルに適合するようにします。
MYCollectionViewController.m
#import "UICollectionView+Draggable.h" @interface MYCollectionViewController () <UICollectionViewDataSource_Draggable, UICollectionViewDelegate> /** * 動物モデル管理リポジトリ */ @property (nonatomic) MYAnimalRepository *repository;
こちらでもStoryboardのUICollectionViewのdataSource, delegateをMYCollectionViewControllerに引っ張ってくるのを忘れないようにしてください。
UICollectionViewDataSource_DraggableプロトコルはUICollectionViewDataSourceを継承しているので3つ適合宣言を書く必要がないことに注意してください。
ViewControllerのロード時にUICollectionViewのカテゴリに新しく追加されたdraggableのプロパティを有効にし、各種DataSource_Draggableメソッドを実装します。
- (void)viewDidLoad { [super viewDidLoad]; //ドラッグできるかどうか決められます self.collectionView.draggable = YES; } #pragma mark - UICollectionViewDataSource_Draggable /** * コレクションビュー内のセルがドラッグして移動した時に呼ばれます * * @param collectionView ドラッグ対象コレクションビュー * @param fromIndexPath 移動元セルインデックスパス * @param toIndexPath 移動先セルインデックスパス */ - (void)collectionView:(UICollectionView *)collectionView moveItemAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath { //モデルに順序入れ替えの指示を出します。 [self.repository moveAnimalAtIndex:fromIndexPath.row toIndex:toIndexPath.row]; } /** * 指定インデックスパスのセルをドラッグで動かしていいかどうか問い合わせます * * @param collectionView ドラッグ対象コレクションビュー * @param indexPath ドラッグ対象セルインデックスパス * * @return 動かしていいかどうか */ - (BOOL)collectionView:(UICollectionView *)collectionView canMoveItemAtIndexPath:(NSIndexPath *)indexPath { return YES; } /** * 指定インデックスパスのセルが移動先のセルに動かせるかどうかが決められます * * @param collectionView ドラッグ対象コレクションビュー * @param indexPath 移動元セルのインデックスパス * @param toIndexPath 移動先セルのインデックスパス * * @return 動かせるかどうか */ - (BOOL)collectionView:(UICollectionView *)collectionView canMoveItemAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)toIndexPath { return YES; }
あとはUICollectionViewDataSourceの他必須メソッドを実装し、データソースモデルが正しくロードされていればセルがドラッグ移動可能になります。
サンプルプロジェクトではドラッグで移動可能になったことがわかりやすいようにさらにコードを追加してセルに対するアニメーションを追加しています。
こちらの記事では解説しませんが、セルのアニメーションの部分で一つだけ補足として、アニメーション中のビューはデフォルトでタップ可能ではないため、UIViewAnimationOptionとして
UIViewAnimationOptionAllowUserInteraction
を指定する必要が有ることを付け加えておきます。